###############################################################################################################################################################################
# Hunger Games Code - written by Nitai Dean
###############################################################################################################################################################################
import numpy as np

# Define all global variables:
###############################################################################################################################################################################
total_decisions = 0 # Total H+S for all previous rounds so far (integer)
game_phase = 1 # which phase of the game are we in, 1 or 2?
player_reputations_previous = [] # list of player reputations from last round (list of floats [0,1])
consecutive_identifications = [] # number of  consecutive identifications (starting with 0), with -1 signaling unidentified players (list of integers)
last_action = [] # last action that the last player did to us, with 'h' for hunt, 's' for slack
group_hunts = [0.0] * 10 # number of times the average player hunts, for each of the last 10 rounds (list of floats)
hunt_decisions_previous = [] # our decisions for each other player from the last round

# First mandatory function
###############################################################################################################################################################################
def hunt_choices(round_number, current_food, current_reputation, m,  player_reputations):
    # The main routine that plays each individual round.
    
    # You must create an array of variables 'hunt_decisions' and assign an 'h' for hunt or
    # an 's' for slack (i.e., not hunt) to each member of the array; the order of the hunt
    # decisions in hunt_decisions should correspond to the order of opponents'
    # reputations in player_reputations.
    
    # Blank variables or errors will be assigned 's'.
    
    # The variables passed in to hunt_choices for your use are:
    #     round_number: integer, the number round you are in.
    #     current_food: integer, the amount of food you have.
    #     current_reputation: float (python's representation of real numbers), your current reputation.
    #     m: integer, the threshold cooperation/hunting value for this round.
    #     player_reputations: list of floats, the reputations of all the remaining players in the game.
    #                         The ordinal positions of players in this list will be randomized each round.
    
    # Declare global variables so they may be altered if needed
    ########################################################################################################################################################################### 
    global total_decisions
    global game_phase
    global player_reputations_previous
    global consecutive_identifications
    global last_action
    global group_hunts
    global hunt_decisions_previous
    
    # Determine whether the game should switch to phase 2 or not
    ###########################################################################################################################################################################         
    if len(player_reputations) != len(player_reputations_previous) and round_number > 1: # the moment the first player dies
        game_phase = 2 # switch to phase 2
        consecutive_identifications = [-1] * len(player_reputations) # initialize list for new method of identification
    
    # Phase 1
    ########################################################################################################################################################################### 
    if game_phase == 1: # naively tit-for-tat based on sorted reputation, with tendancy towards generosity
        
        # Benefit-of-the-doubt tit for tat on ordered reputations
        #######################################################################################################################################################################
        sorted_indices = list(np.argsort(player_reputations)) # index that sorts player reputations in ascending order
        if round_number in [1,2]: # for first 2 rounds
            hunt_decisions = ['h'] * len(player_reputations) # hunt unconditionally with everyone
        else: # round 3 and onwards  
            hunt_decisions = ['h'] * len(player_reputations) # initialize to hunt
            for i in range(len(player_reputations)): # for each slot in the sorted reputation list
                # we want to slack with someone only if both his neighbors in the sorted list slacked as well
                # meaning: we're looking for an excuse to be nice
                if i == 0: # if player is first, only one neighbor
                    if last_action[i] == 's' and last_action[i+1] == 's': # if player and neighbor slacked
                        hunt_decisions[sorted_indices[i]] = 's' # slack back
                elif i == len(player_reputations) - 1: # if player is last, only one neighbor
                    if last_action[i] == 's' and last_action[i-1] == 's': # if player and neighbor slacked
                        hunt_decisions[sorted_indices[i]] = 's' # slack back
                else: # if player is in the middle, he has 2 neighbors
                    if last_action[i] == 's' and last_action[i-1] == 's' and last_action[i+1] == 's': # if player and neighbors slacked
                        hunt_decisions[sorted_indices[i]] = 's' # slack back
        
        # Update global variables for next round
        #######################################################################################################################################################################  
        player_reputations_previous = [player_reputations[k] for k in sorted_indices] # Storing the sorted reps from this round for the next round
        hunt_decisions_previous = [hunt_decisions[k] for k in sorted_indices] # Storing the sorted decisions from this round for the next round
    
    # Phase 2
    ########################################################################################################################################################################### 
    else: # begin more involved tracking of players as well as maneuvering to an isolated reputation
        
        # Convert our reputation and that of other players to integer values representing hunts
        #######################################################################################################################################################################  
        my_total_hunts = int(current_reputation * total_decisions)
        total_hunts = [int(x * total_decisions) for x in player_reputations]
        total_decisions_previous = total_decisions - len(player_reputations_previous)
        total_hunts_previous = [int(x * total_decisions_previous) for x in player_reputations_previous]
        
        # Assign likely boundries for each player's rep from the previous round to where they might be this round
        ####################################################################################################################################################################### 
        player_boundries = list() # will hold a tuple for each player from last round indicating low and high boundry range
        for i in range(len(player_reputations_previous)): # for each player last round
            boundries = (total_hunts_previous[i], total_hunts_previous[i] + len(player_reputations_previous)) # which range you will expect him in
            player_boundries.append(boundries) # add to list, which has same index as player_reputations_previous
        
        # Determine which players can be identified
        #######################################################################################################################################################################  
        consecutive_identifications_next = [-1] * len(player_reputations) # list like consecutive_identifications but updated to this round, with the new ordering
        last_action_next = ['u'] * len(player_reputations) # list like last_action but updated to this round, with the new ordering. initialize to "unknown"
        for i in range(len(player_reputations_previous)): # for each player last round
            currentPlayers = [{'index':j, 'hunts':x} for (j,x) in enumerate(total_hunts) if x >= player_boundries[i][0] and x <= player_boundries[i][1]]
            # find all new players that are in the old player's boundries
            if len(currentPlayers) == 1: # if only one new player in the old player's boundry, then we found him
                if consecutive_identifications[i] == -1: # player was previously unidentified, this is his first identification
                    consecutive_identifications_next[currentPlayers[0]['index']] = 0 # he now has 1 consecutive identification
                    last_action_next[currentPlayers[0]['index']] = last_action[i] # copy last action to new ordering
                else: # player was already identified
                    consecutive_identifications_next[currentPlayers[0]['index']] = (consecutive_identifications[i] + 1) % 10 # increment consecutive identification
                    last_action_next[currentPlayers[0]['index']] = last_action[i] # copy last action to new ordering
        
        # Initialize variables
        #######################################################################################################################################################################
        hunt_decisions = ['u'] * len(player_reputations) # initialize on undecided
        direction = 'stay' # initialize chosen direction
        danger_above_P = 0 # how many players above us are within P hunts from us
        danger_above_2P = 0 # how many players above us are within 2P hunts from us
        danger_below_P = 0 # how many players below us are within P hunts from us
        danger_below_2P = 0 # how many players below us are within 2P hunts from us
        
        # Tally up danger indicators for later deciding direction of movement, and hand out hunts/slacks to identified players
        #######################################################################################################################################################################
        for i in range(len(player_reputations)): # for each player this round
            if total_hunts[i] >= my_total_hunts: # this player is above me in reputation
                if total_hunts[i] - my_total_hunts < len(player_reputations): # if this player is closer than P hunts from me
                    danger_above_P += 1 
                if total_hunts[i] - my_total_hunts < 2 * len(player_reputations): # if this player is closer than 2P hunts from me
                    danger_above_2P += 1 
            else: # this player is below me in reputation
                if my_total_hunts - total_hunts[i] < len(player_reputations): # if this player is closer than P hunts from me
                    danger_below_P += 1 
                if my_total_hunts - total_hunts[i] < 2 * len(player_reputations): # if this player is closer than 2P hunts from me
                    danger_below_2P += 1 
            if consecutive_identifications_next[i] in [3,4]: # twice every 10 consecutive identifications hunt unconditionally
                hunt_decisions[i] = 'h'
            elif consecutive_identifications_next[i] == -1: # target is unidentified, leave him on undecided
                pass
            else: # target is identified and not eligible for 2 unconditional hunts
                if last_action_next[i] == 's': # if he slacked on us last round
                    hunt_decisions[i] = 's' # slack him back
                elif last_action_next[i] == 'h': # if last round he hunted with us
                    hunt_decisions[i] = 'h' # hunt with him
                if player_reputations[i] in [0,1]: # if player has a pure reputation of always hunting or always slacking
                    hunt_decisions[i] = 's' # always slack on him
        
        # Decide which direction is better to move towards on the reputation axis (including staying in place)
        #######################################################################################################################################################################
        num_hunts = sum([x == 'h' for x in hunt_decisions]) # number of hunts given so far
        num_undecided = sum([x == 'u' for x in hunt_decisions]) # number of decisions left to make
        rep_change_max = num_hunts + num_undecided - int(round(np.mean(group_hunts))) # total potential hunts over the average
        rep_change_min = num_hunts - int(round(np.mean(group_hunts))) # total potential hunts under the average
        if rep_change_max > 0 and rep_change_min > 0: # if we can only increase our reputation
            if danger_above_P > 0 or danger_below_P > 0: # there is immediate danger above or below us
                direction = 'up' # rush up to escape it
            else: # no immediate danger
                if danger_below_2P > 0 and danger_above_2P == 0: # if long-term danger below and no danger above, go up
                    direction = 'up' # run away from eventual danger from below
        elif rep_change_max < 0 and rep_change_min < 0: # if we can only decrease our reputation
            if danger_above_P > 0 or danger_below_P > 0: # there is immediate danger above or below us
                direction = 'down' # rush down to escape it
            else: # no immediate danger
                if danger_above_2P > 0 and danger_below_2P == 0: # if long-term danger above and no danger below, go down
                    direction = 'down' # run away from eventual danger from above
        else: # we can move up or down
            if danger_above_P > danger_below_P: # there is more immediate danger above than below
                direction = 'down' # rush down to escape it
            elif danger_below_P > danger_above_P: # there is more immediate danger below than above
                direction = 'up' # rush up to escape it   
            elif danger_above_P == danger_below_P and danger_above_P > 0: # there is equal danger from both sides
                if abs(rep_change_max) >= abs(rep_change_min): # if we can move faster up
                    direction = 'up' # rush up to escape
                else: # if we can move faster down
                    direction = 'down' # rush down to escape
            else: # there is no immediate danger
                if danger_above_2P > 0 and danger_below_2P == 0: # if long-term danger above and no danger below, go down
                    direction = 'down' # run away from eventual danger from above
                elif danger_below_2P > 0 and danger_above_2P == 0: # if long-term danger below and no danger above, go up
                    direction = 'up' # run away from eventual danger from below 
        
        # Depending on direction we want to move, issue out remaining hunt/slack orders
        #######################################################################################################################################################################
        if direction == 'down':  
            indices = [i for (i,x) in enumerate(hunt_decisions) if x == 'u']
            for idx in indices:
                hunt_decisions[idx] = 's' # assign all undecided to slacks
        elif direction == 'up': 
            indices = [i for (i,x) in enumerate(hunt_decisions) if x == 'u']
            for idx in indices:
                hunt_decisions[idx] = 'h' # assign all undecided to hunts
        else: # we want to stay in place
            additional_hunts_needed = int(round(np.mean(group_hunts))) - num_hunts # how many hunts we need to add to achieve the average
            if additional_hunts_needed <= 0: # if this is non positive, we don't need any more hunts
                indices = [i for (i,x) in enumerate(hunt_decisions) if x == 'u']
                for idx in indices:
                    hunt_decisions[idx] = 's' # assign all undecided to slacks
            else: # we need to add more hunts
                if additional_hunts_needed > num_undecided: # cannot give more hunts than remaining decisions
                    additional_hunts_needed = num_undecided # cap off at num_undecided
                indices = list(np.argsort(player_reputations)[::-1][range(additional_hunts_needed)]) # indices we want to assign hunts
                # we assign hunts with priority towards the players with highest reputations
                for idx in indices:
                    hunt_decisions[idx] = 'h' # assign chosen players a hunt
                indices = [i for (i,x) in enumerate(hunt_decisions) if x == 'u'] # check if there are remaining undecided
                if len(indices) > 0: # if there are, give them slacks
                    for idx in indices:
                        hunt_decisions[idx] = 's' # assign all remaining undecided to slacks
        
        # If only one player left, slack all the time
        #######################################################################################################################################################################
        if len(player_reputations) == 1:
            hunt_decisions = 's'
        
        # Update global variables for next round
        #######################################################################################################################################################################  
        player_reputations_previous = player_reputations
        hunt_decisions_previous = hunt_decisions
        consecutive_identifications = consecutive_identifications_next # storing the identified players for the next round             
    
    # Return final decisions and end function
    ########################################################################################################################################################################### 
    total_decisions += len(player_reputations) # Incrementing total decisions by P (H+S = P)
    return hunt_decisions

# Second mandatory function
###############################################################################################################################################################################
def hunt_outcomes(food_earnings):
    # hunt_outcomes is called after all hunts for the round are complete.
    
    # Add any code you wish to modify your variables based on the outcome of the last round.
    
    # The variable passed in to hunt_outcomes for your use is:
    #     food_earnings: list of integers, the amount of food earned from the last round's hunts.
    #                    The entries can be negative as it is possible to lose food from a hunt.
    #                    The amount of food you have for the next round will be current_food
    #                    + sum of all entries of food_earnings + award from round_end.
    #                    The list will be in the same order as the decisions you made in that round.
    
    global hunt_decisions_previous
    global last_action
    
    # Update global variable last_action, which can be inferred from food_earnings
    ########################################################################################################################################################################### 
    last_action = ['s'] * len(hunt_decisions_previous) # initialize to slack
    for i in range(len(hunt_decisions_previous)): # for every action we made in the previous round
        if (hunt_decisions_previous[i] == 'h' and food_earnings[i] == 0) or (hunt_decisions_previous[i] == 's' and food_earnings[i] == 1): 
            # if we hunted and earned 0 food OR we slacked and earned 1 food, then he must have hunted
            last_action[i] = 'h'

# Third mandatory function
###############################################################################################################################################################################
def round_end(award, m, number_hunters):
    # round_end is called after all hunts for the round are complete.
    
    # award - the total amount of food you received due to cooperation in the round.
    # Can be zero if the threshold m was not reached.
    
    # Add any code you wish to modify your variables based on the cooperation that occurred in
    # the last round.
    
    # The variables passed in to round_end for your use are:
    #     award: integer, total food bonus (can be zero) you received due to players cooperating
    #            during the last round. The amount of food you have for the next round will be
    #            current_food (including food_earnings from hunt_outcomes this round) + award.
    #     number_hunters: integer, number of times players chose to hunt in the last round.
    
    # Update global variable group_hunts, which can be inferred from number_hunters
    ########################################################################################################################################################################### 
    global hunt_decisions_previous
    global group_hunts
    
    group_hunts.append(float(number_hunters) / (len(hunt_decisions_previous) + 1)) # number of avg hunts in this past round
    del(group_hunts[0])

